IT Ops Portal
Operator & IT Manager Console
SLA Breach 30d
–
violazioni SLA
Clicca "Analizza" per correlare gli incident aperti e suggerire la causa radice.
CHANGE RISK ASSESSMENT
Inserisci la chiave di un change RFC e clicca "Valuta Rischio AI".
🧊 Change Freeze attivo — –
Ultimi CI aggiornati da discovery
📡 Webhook Endpoint — Discovery Esterna
–
Invia un POST a questo URL con il payload JSON. Compatibile con Zabbix Action, Nagios, Nmap NSE script, agenti custom.
Mostra payload di esempio
{
"project": "TENANT",
"source": "zabbix",
"cis": [
{
"hostname": "srv-app-01.azienda.it",
"ip": "10.0.1.10",
"type": "Server",
"os": "Ubuntu 24.04 LTS",
"environment": "Production",
"criticality": "High",
"services": "nginx, postgresql"
}
]
}
SLA BREACH PREDICTION
Analisi in corso...
COMPLIANCE ANALYSIS
Clicca per analizzare la postura di compliance.
Invia i findings del tuo scanner a questo endpoint. Compatibile con Tenable, Qualys, Nuclei, Trivy, Grype, Snyk e webhook personalizzati.
–
Mostra payload di esempio
{
"project": "TENANT",
"source": "nuclei",
"findings": [
{
"cve_id": "CVE-2024-12345",
"cvss": 9.8,
"severity": "Critical",
"title": "Remote Code Execution in Apache HTTP Server",
"host": "web-01.azienda.it",
"component": "apache/httpd:2.4.51",
"exploit_available": true,
"remediation_days": 7
}
]
}
Clicca "Esegui AIOps Now" per avviare la correlazione automatica degli incident aperti.
🎯 Ticket Triage
Classificazione automatica dei nuovi ticket aperti
🔍 RCA Assist
Correlazione incident → causa radice comune
⏱ SLA Breach Predict
Ticket a rischio SLA nelle prossime 4 ore
🚨 Anomaly Detection
Spike e degradazioni anomale per servizio
📊 Capacity Forecast
Previsione volume ticket prossime 4 settimane
📋 Runbook Generator
Genera runbook da storico ticket simili
⭐ Sentiment CSAT
Analisi soddisfazione utenti da CSAT
🛡 Compliance Check
Stato compliance GDPR/ISO27001/NIS2
📈 Incident Trend
Trend e predizioni 24h
ARTICOLO GENERATO
Inserisci la chiave di un ticket risolto per generare un articolo KB.
TREND ANALYSIS
Clicca per analizzare i trend con AI.
async function loadBridges() {
const el = document.getElementById('bridge-list');
if (!el) return;
el.innerHTML = '';
try {
const r = await workerGet(`/api/itsm/active-bridges?project=${P()}`);
const bridges = r.bridges || [];
// KPI
const active = bridges.length;
const avgAge = active > 0 ? Math.round(bridges.reduce((s,b) => s + b.age_minutes, 0) / active) : 0;
const comms = bridges.filter(b => b.comms_sent === 'Yes').length;
const pir = bridges.filter(b => b.pir_date).length;
document.getElementById('bri-kpi-active').textContent = active;
document.getElementById('bri-kpi-age').textContent = avgAge || '–';
document.getElementById('bri-kpi-comms').textContent = comms;
document.getElementById('bri-kpi-pir').textContent = pir;
// Badge sidebar
const badge = document.getElementById('bridge-badge');
if (badge) { badge.style.display = active > 0 ? '' : 'none'; badge.textContent = active; }
if (bridges.length === 0) {
el.innerHTML = '✅
Nessun Major Incident attivo
Tutti i sistemi operativi
';
return;
}
el.innerHTML = bridges.map(b => {
const ageColor = b.age_minutes > 120 ? 'var(--red)' : b.age_minutes > 60 ? 'var(--orange)' : 'var(--green)';
const ageLabel = b.age_minutes >= 60
? `${Math.floor(b.age_minutes/60)}h ${b.age_minutes%60}m`
: `${b.age_minutes}min`;
return `
${b.key}
BRIDGE ATTIVO
⏱ ${ageLabel}
${b.summary}
Commander: ${b.commander} · Servizio: ${b.affected_service||'–'}
${b.status_update ? `
Ultimo aggiornamento: ${b.status_update}
` : ''}
Comms clienti: ${b.comms_sent}
${b.pir_date ? `PIR: ${new Date(b.pir_date).toLocaleDateString('it-IT')}` : ''}
Stato: ${b.status}
`;
}).join('');
} catch(e) { el.innerHTML = '⚠️
Errore caricamento bridge
'; }
}
async function openBridgeDetail(key) {
try {
const r = await workerGet(`/api/issue?key=${key}`);
const f = r.issue?.fields || {};
document.getElementById('modal-bridge-key').textContent = `🌉 ${key} — Major Incident Bridge`;
document.getElementById('modal-bridge-body').innerHTML = `
${f.priority?.name||'–'}
${f.status?.name||'–'}
${f.summary||'–'}
Commander:
${f.customfield_incident_commander?.displayName||f.assignee?.displayName||'–'}
Servizio impattato:
${f.customfield_affected_service||'–'}
Comms clienti inviate:
${f.customfield_customer_comms_sent?.value||'No'}
PIR pianificata:
${f.customfield_pir_date?new Date(f.customfield_pir_date).toLocaleDateString('it-IT'):'Da pianificare'}
RCA:
${f.customfield_rca||'In corso...'}
${f.customfield_bridge_status_update ? `
Ultimo status update
${f.customfield_bridge_status_update}
` : ''}
${f.customfield_bridge_participants ? `
Partecipanti bridge
${f.customfield_bridge_participants}
` : ''}
`;
openModal('modal-bridge');
} catch(e) { nhToast('Errore caricamento bridge', 'error'); }
}
function declareMajorIncident() {
nhToast('Apri un ticket IT-MAJ in Jira per dichiarare un Major Incident — il bridge verrà creato automaticamente.', 'warning');
window.open(`${ITSMOPS_CONFIG.jira_base_url}/jira/software/projects/${P()}/boards`, '_blank');
}
// ══════════════════════════════════════════════════════════
// ON-CALL SCHEDULE
// ══════════════════════════════════════════════════════════
async function loadOnCall() {
const nowEl = document.getElementById('oncall-now');
const calEl = document.getElementById('oncall-calendar-list');
if (!nowEl || !calEl) return;
try {
const today = new Date().toISOString().split('T')[0];
const [nowR, calR] = await Promise.all([
workerGet(`/api/itsm/oncall-schedule?project=${P()}&date=${today}`),
workerGet(`/api/itsm/oncall-calendar?project=${P()}&weeks=4`),
]);
// Turno attivo ora
const active = nowR.schedules || [];
if (active.length === 0) {
nowEl.innerHTML = 'Nessun turno configurato per oggi.
';
} else {
nowEl.innerHTML = active.map(s => `
${s.assignee.split(' ').map(w=>w[0]).slice(0,2).join('')}
${s.assignee}
${s.rotation} · Tier ${s.tier} · Escalation: ${s.escalation_policy}
${s.contact_phone ? `
📞 ${s.contact_phone}` : ''}
Turno attivo
`).join('
');
}
// Calendario
const calendar = calR.calendar || [];
if (calendar.length === 0) {
calEl.innerHTML = '📅
Nessun turno pianificato
Aggiungi turni per le prossime settimane
';
return;
}
const tierColor = { L1:'var(--blue)', L2:'var(--purple)', L3:'var(--orange)', Manager:'var(--red)' };
calEl.innerHTML = `
| Oggetto | Responsabile | Rotazione | Tier |
Inizio | Fine | Telefono |
${calendar.map(i => {
const f = i.fields;
const tier = f.customfield_oncall_tier?.value || 'L1';
const start = f.customfield_oncall_schedule_date ? new Date(f.customfield_oncall_schedule_date).toLocaleString('it-IT',{day:'2-digit',month:'short',hour:'2-digit',minute:'2-digit'}) : '–';
const end = f.customfield_oncall_end_date ? new Date(f.customfield_oncall_end_date).toLocaleString('it-IT',{day:'2-digit',month:'short',hour:'2-digit',minute:'2-digit'}) : '–';
return `
| ${f.summary||i.key} |
${f.assignee?.displayName||'Non assegnato'} |
${f.customfield_oncall_rotation?.value||'–'} |
${tier} |
${start} |
${end} |
${f.customfield_oncall_contact_phone||'–'} |
`;
}).join('')}
`;
} catch(e) {
if (nowEl) nowEl.innerHTML = 'Errore caricamento
';
if (calEl) calEl.innerHTML = '';
}
}
async function submitOnCallTurno() {
const summary = document.getElementById('oc-summary')?.value?.trim();
const start = document.getElementById('oc-start')?.value;
const end = document.getElementById('oc-end')?.value;
const rotation = document.getElementById('oc-rotation')?.value;
const tier = document.getElementById('oc-tier')?.value;
const phone = document.getElementById('oc-phone')?.value?.trim();
const escalation = document.getElementById('oc-escalation')?.value;
if (!summary || !start || !end) { nhToast('Compila oggetto, inizio e fine turno', 'warning'); return; }
try {
const fields = {
project: { key: P() },
issuetype: { name: `${P()}-IT-ONCALL` },
summary,
customfield_oncall_schedule_date: start,
customfield_oncall_end_date: end,
customfield_oncall_rotation: rotation ? { value: rotation } : undefined,
customfield_oncall_tier: tier ? { value: tier } : undefined,
customfield_oncall_contact_phone: phone || undefined,
customfield_oncall_escalation_policy: escalation ? { value: escalation } : undefined,
};
// Rimuovi campi undefined
Object.keys(fields).forEach(k => fields[k] === undefined && delete fields[k]);
const r = await workerPost('/api/issue', { body: { fields } });
if (r.key) {
nhToast(`Turno ${r.key} creato!`, 'success');
closeModal('modal-oncall');
loadOnCall();
}
} catch(e) { nhToast('Errore creazione turno: ' + (e.message||''), 'error'); }
}
function openNewOnCallModal() {
// Pre-compila data/ora di default (ora corrente + 8 ore)
const now = new Date();
const end = new Date(now.getTime() + 8 * 3600000);
const fmt = d => d.toISOString().slice(0,16);
const startEl = document.getElementById('oc-start');
const endEl = document.getElementById('oc-end');
if (startEl) startEl.value = fmt(now);
if (endEl) endEl.value = fmt(end);
openModal('modal-oncall');
}
// ══ HOOK: Aggiorna showPage per Bridge e OnCall ════════════
const _showPageOrig2 = typeof showPage === 'function' ? showPage : null;
if (_showPageOrig2) {
const _prevShowPage = window.showPage || _showPageOrig2;
window.showPage = function(name) {
_prevShowPage(name);
if (name === 'bridges') loadBridges();
if (name === 'oncall') loadOnCall();
if (name === 'sla') loadSLAFull();
if (name === 'pipeline') loadPipeline();
const titles2 = { bridges: 'Major Incident Bridge', oncall: 'On-Call Schedule', pipeline: 'Release Pipeline' };
if (titles2[name]) document.getElementById('topbar-title').textContent = titles2[name];
};
}
// ══ RELEASE PIPELINE ═══════════════════════════════════════════════════════
const PIPELINE_ENV_META = {
"Dev": { color: "#6366f1", bg: "#eef2ff", label: "DEV" },
"Test": { color: "#0891b2", bg: "#ecfeff", label: "TEST" },
"Staging": { color: "#d97706", bg: "#fffbeb", label: "STAGING" },
"Pre-Production": { color: "#7c3aed", bg: "#f5f3ff", label: "PRE-PROD" },
"Production": { color: "#059669", bg: "#ecfdf5", label: "PROD" },
};
const DEPLOY_STATUS_STYLE = {
"Success": { icon: "✅", badge: "badge-green" },
"Failed": { icon: "❌", badge: "badge-red" },
"Rolled Back":{ icon: "↩", badge: "badge-orange" },
"In Progress":{ icon: "⏳", badge: "badge-blue" },
"Cancelled": { icon: "⛔", badge: "badge-gray" },
};
async function loadPipeline() {
const days = document.getElementById('pipeline-days')?.value || '30';
const board = document.getElementById('pipeline-board');
if (board) board.innerHTML = '';
try {
const r = await workerGet(`/api/itsm/release-pipeline?project=${P()}&days=${days}`);
// Aggiorna KPI globali
const s = r.summary || {};
document.getElementById('pip-total').textContent = s.total ?? '–';
document.getElementById('pip-rate').textContent = s.success_rate != null ? s.success_rate : '–';
document.getElementById('pip-failed').textContent = s.failed ?? '–';
document.getElementById('pip-rollback').textContent = s.rollbacks ?? '–';
// Colora KPI rate
const rateEl = document.getElementById('pip-rate');
if (s.success_rate != null) {
rateEl.className = 'kpi-value ' + (s.success_rate >= 90 ? 'green' : s.success_rate >= 70 ? 'orange' : 'red');
}
// Render board
if (board) board.innerHTML = (r.columns || []).map(col => renderPipelineColumn(col)).join('');
} catch(e) {
if (board) board.innerHTML = '⚠️
Errore caricamento pipeline
';
}
}
function renderPipelineColumn(col) {
const meta = PIPELINE_ENV_META[col.environment] || { color: "#64748b", bg: "#f8fafc", label: col.environment };
const rateColor = col.success_rate == null ? '#64748b'
: col.success_rate >= 90 ? '#059669'
: col.success_rate >= 70 ? '#d97706' : '#dc2626';
const headerKpis = col.total > 0 ? `
✅ ${col.success}
${col.failed ? `❌ ${col.failed}` : ''}
${col.rollbacks ? `↩ ${col.rollbacks}` : ''}
${col.in_progress ? `⏳ ${col.in_progress}` : ''}
` : '';
const deployCards = col.deploys.length === 0
? 'Nessun deploy
'
: col.deploys.slice(0, 8).map(d => {
const ds = DEPLOY_STATUS_STYLE[d.deploy_status] || { icon: "•", badge: "badge-gray" };
const sha = d.commit_sha ? d.commit_sha.substring(0, 7) : '';
const dur = d.duration_min != null ? `${d.duration_min}m` : '';
const dateStr = d.created ? new Date(d.created).toLocaleDateString('it-IT', { day:'2-digit', month:'short' }) : '';
return `
${d.key}
${ds.icon}
${(d.build_id || d.summary).substring(0, 40)}${(d.build_id||d.summary).length > 40 ? '…' : ''}
${sha ? `${sha}` : ''}
${dur ? `⏱ ${dur}` : ''}
${dateStr ? `${dateStr}` : ''}
${d.rollback ? `↩ Rollback` : ''}
${d.pipeline_url ? `
→ Pipeline` : ''}
`;
}).join('');
return `
${meta.label}
${col.success_rate != null ? col.success_rate + '%' : '–'}
${col.total} deploy${col.avg_duration_min ? ` · avg ${col.avg_duration_min}m` : ''}
${headerKpis}
${deployCards}
`;
}
// ══════════════════════════════════════════════════════════
// AIOPS — Correlation, Anomaly Detection, Capacity Forecast
// ══════════════════════════════════════════════════════════
async function runAIOpsCorrelation() {
const panel = document.getElementById('aiops-correlation-panel');
if (!panel) return;
panel.innerHTML = 'Analisi correlazione in corso…
';
document.getElementById('aiops-kpi-clusters').textContent = '…';
document.getElementById('aiops-kpi-highconf').textContent = '…';
document.getElementById('aiops-kpi-problems').textContent = '…';
try {
const r = await workerPost('/api/itsm/aiops-correlate', { project: P() });
// KPI
document.getElementById('aiops-kpi-clusters').textContent = r.total_clusters ?? 0;
document.getElementById('aiops-kpi-highconf').textContent = r.high_confidence_count ?? 0;
document.getElementById('aiops-kpi-problems').textContent = r.auto_created_problem ? '1 ✓' : '0';
const clusters = r.clusters || [];
if (!clusters.length) {
panel.innerHTML = '✅ Nessuna correlazione significativa rilevata — ' + (r.incidents_analyzed||0) + ' incident analizzati.
';
return;
}
const confColor = c => c >= 80 ? 'var(--red)' : c >= 60 ? 'var(--orange)' : 'var(--blue)';
const actionLabel = { create_problem:'🔴 Crea Problem', merge_tickets:'🔗 Mergia ticket', escalate:'⬆ Escalation', monitor:'👁 Monitora' };
panel.innerHTML = `
${r.incidents_analyzed||0} incident analizzati · ${clusters.length} cluster trovati · Timestamp: ${r.timestamp ? new Date(r.timestamp).toLocaleTimeString('it-IT') : '–'}
${r.auto_created_problem ? `✅ Problem record creato automaticamente: ${r.auto_created_problem}
` : ''}
${clusters.map((c, i) => `
Cluster ${i+1}
Score: ${c.correlation_score}%
${actionLabel[c.recommended_action]||c.recommended_action}
Confidenza: ${c.confidence}%
🔧 Servizio: ${c.common_service||'–'}
📁 Categoria: ${c.common_category||'–'}
Root cause probabile: ${c.probable_root_cause}
${(c.incident_keys||[]).map(k => `
${k}`).join('')}
`).join('')}`;
} catch(e) {
panel.innerHTML = `Errore AIOps: ${e.message}
`;
document.getElementById('aiops-kpi-clusters').textContent = '–';
}
}
async function runAIOpsAnomaly() {
const el = document.getElementById('ai-tools-text');
const title = document.getElementById('ai-tools-result-title');
if (title) title.textContent = '🚨 Anomaly Detection';
if (el) el.textContent = 'Analisi anomalie in corso…';
try {
const r = await workerGet(`/api/itsm/ai-anomaly?project=${P()}`);
document.getElementById('aiops-kpi-anomalies').textContent = r.total_anomalies ?? 0;
const anomalies = r.anomalies || [];
if (!anomalies.length) {
if (el) el.textContent = `✅ Nessuna anomalia rilevata nel periodo analizzato. Incident correnti: ${r.current_period}, baseline: ${r.baseline_period}.`;
return;
}
const sevColor = { critical:'var(--red)', high:'var(--orange)', medium:'var(--blue)', low:'var(--green)' };
if (el) el.innerHTML = `
${r.current_period} incident (7gg) vs ${r.baseline_period} baseline · Generato: ${r.generated_at ? new Date(r.generated_at).toLocaleString('it-IT') : '–'}
${r.immediate_action_required ? '⚠️ Azione immediata richiesta
' : ''}
${anomalies.map(a => `
${a.service}
${a.severity}
${a.type}
${a.description}
Baseline: ${a.baseline_count} → Corrente: ${a.current_count} (score: ${a.anomaly_score})
→ ${a.recommended_action}
`).join('')}`;
} catch(e) {
if (el) el.textContent = 'Errore anomaly detection: ' + e.message;
}
}
async function runAIOpsCapacity() {
const el = document.getElementById('ai-tools-text');
const title = document.getElementById('ai-tools-result-title');
if (title) title.textContent = '📊 Capacity Forecast';
if (el) el.textContent = 'Generazione forecast in corso…';
try {
const r = await workerGet(`/api/itsm/ai-capacity-forecast?project=${P()}`);
const forecast = r.forecast || [];
if (!forecast.length) {
if (el) el.textContent = r.error || 'Dati storici insufficienti per generare un forecast.';
return;
}
if (el) el.innerHTML = `
Forecast basato su ${r.historical_weeks} settimane storiche · Generato: ${r.generated_at ? new Date(r.generated_at).toLocaleString('it-IT') : '–'}
| Settimana | Incident previsti | SR previste | Confidenza |
${forecast.map(f => `
| ${f.week} |
${f.predicted_incidents} |
${f.predicted_sr} |
${f.confidence}% |
`).join('')}
${r.staff_recommendation ? `Raccomandazione staff: ${r.staff_recommendation}
` : ''}
${r.risk_weeks?.length ? `⚠️ Settimane a rischio: ${r.risk_weeks.join(', ')}
` : ''}
${r.key_drivers?.length ? `Driver principali: ${r.key_drivers.join(' · ')}
` : ''}`;
} catch(e) {
if (el) el.textContent = 'Errore capacity forecast: ' + e.message;
}
}
async function runSelfHeal() {
const key = document.getElementById('selfheal-key')?.value?.trim();
if (!key) { nhToast('Inserisci la chiave ticket', 'warning'); return; }
const el = document.getElementById('selfheal-result');
if (!el) return;
el.style.display = 'block';
el.innerHTML = 'Generazione runbook AI in corso…
';
try {
const r = await workerPost('/api/ai-analyze', {
type: 'self-heal', project: P(),
context: { ticket_key: key, message: `Genera runbook self-heal per ticket ${key}` }
});
const result = r.result ? (typeof r.result === 'string' ? JSON.parse(r.result) : r.result) : {};
const analysis = result.incident_analysis || {};
const runbook = result.runbook || {};
const steps = runbook.steps || [];
el.innerHTML = `
${runbook.title || 'Runbook AI — ' + key}
🎯 Root cause: ${analysis.root_cause_hypothesis||'–'}
⏱ ETA: ${runbook.estimated_resolution_min||'–'} min
🤖 Automazione: ${result.automation_coverage_pct||0}%
📊 Confidenza: ${analysis.confidence||0}%
${steps.map(s => `
${s.step}
${s.action}
${s.command_or_api ? `
${s.command_or_api}` : ''}
✓ ${s.verification}
${s.requires_approval ? '
Richiede approvazione' : ''}
${s.automated?'AUTO':'MANUALE'}
`).join('')}
${result.escalation_required ? `
⬆ Escalation richiesta: ${result.escalation_reason}
` : ''}
`;
} catch(e) {
el.innerHTML = `Errore: ${e.message}
`;
}
}
// ══ HOOK showPage per AI-tools ═══════════════════════════
const _showPageOrig5 = window.showPage;
if (_showPageOrig5) {
window.showPage = function(name) {
_showPageOrig5(name);
if (name === 'ai-tools') {
document.getElementById('topbar-title').textContent = 'AI Tools & AIOps';
}
};
}
// ══════════════════════════════════════════════════════════
// i18n Operator — lingua sincronizzata con Employee
// ══════════════════════════════════════════════════════════
(function initOpsLang() {
try {
const lang = localStorage.getItem('nh_lang') || ITSMOPS_CONFIG.language || 'it';
if (lang !== 'it') {
const langLabels = { en: 'EN', es: 'ES', pt: 'PT', it: 'IT' };
const topbarUser = document.getElementById('topbar-uname');
if (topbarUser) {
const pill = document.createElement('span');
pill.style.cssText = 'font-size:10px;padding:1px 6px;border-radius:20px;background:var(--surface-3);color:var(--text-3);margin-left:4px;cursor:pointer';
pill.textContent = langLabels[lang] || 'IT';
pill.title = 'Language: ' + lang.toUpperCase();
topbarUser.parentElement?.appendChild(pill);
}
}
} catch(e) {}
})();
// ══════════════════════════════════════════════════════════
// PWA — Operator portal install prompt
// ══════════════════════════════════════════════════════════
(function registerOpsPWA() {
if (!document.querySelector('meta[name="theme-color"]')) {
const meta = document.createElement('meta');
meta.name = 'theme-color'; meta.content = '#1a1a2e';
document.head.appendChild(meta);
}
if (!document.querySelector('link[rel="manifest"]')) {
const link = document.createElement('link');
link.rel = 'manifest';
link.href = 'data:application/json,' + encodeURIComponent(JSON.stringify({
name: (ITSMOPS_CONFIG.project_name || 'NH') + ' IT Ops',
short_name: 'IT Ops',
start_url: './',
display: 'standalone',
background_color: '#1a1a2e',
theme_color: '#156082',
icons: [
{ src: 'https://via.placeholder.com/192x192/156082/ffffff?text=OPS', sizes: '192x192', type: 'image/png' },
{ src: 'https://via.placeholder.com/512x512/156082/ffffff?text=OPS', sizes: '512x512', type: 'image/png' },
]
}));
document.head.appendChild(link);
}
})();